﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace ABDLight
{
    public static class EncryptAndCompressUtility
    {
        /// <summary>
        /// Encrypts and compresses the input. Returns the compressed and encrypted content and the key and iv vector used.
        /// </summary>
        /// <param name="input">The input array.</param>
        /// <param name="key">The key that has been used by the algorithm.</param>
        /// <param name="iv">The iv vector that has been used by the algorithm.</param>
        /// <returns></returns>
        public static byte[] EncryptAndCompress(byte[] input, out byte[] key, out byte[] iv)
        {
            if (input == null)
                throw new ArgumentNullException("input");
            // Compress the input array into the given memory stream.
            MemoryStream stream = new MemoryStream();
            using (GZipStream zip = new GZipStream(stream, CompressionMode.Compress, true))
            {
                // Write the input to the memory stream via the ZIP stream.
                zip.Write(input, 0, input.Length);
            }

            // Create the keys and initalize the rijndael.
            RijndaelManaged r = new RijndaelManaged();
            r.GenerateKey();
            r.GenerateIV();
            // Set the generated key and iv vector.
            key = r.Key;
            iv = r.IV;

            // Encrypt the compressed memory stream into the encrypted memory stream.
            MemoryStream encrypted = new MemoryStream();
            using (CryptoStream cryptor = new CryptoStream(encrypted, r.CreateEncryptor(), CryptoStreamMode.Write))
            {
                // Write the stream to the encrypted memory stream.
                cryptor.Write(stream.ToArray(), 0, (int)stream.Length);
                cryptor.FlushFinalBlock();
                // Return the result.
                return encrypted.ToArray();
            }
        }

        /// <summary>
        /// Decrypts and decompresses the input. Returns the decompressed and decrypted content.
        /// </summary>
        /// <param name="input">The input array.</param>
        /// <param name="key">The key used for decrypt.</param>
        /// <param name="iv">The iv vector used for decrypt.</param>
        /// <returns></returns>
        public static byte[] DecryptAndDecompress(byte[] input, byte[] key, byte[] iv)
        {
            if (input == null)
                throw new ArgumentNullException("input");
            if (key == null)
                throw new ArgumentNullException("key");
            if (iv == null)
                throw new ArgumentNullException("iv");

            // Initialize the rijndael
            RijndaelManaged r = new RijndaelManaged();
            // Create the array that holds the result.
            byte[] decrypted = new byte[input.Length];
            // Create the crypto stream that is used for decrypt. The first argument holds the input as memory stream.
            using (CryptoStream decryptor = new CryptoStream(new MemoryStream(input), r.CreateDecryptor(key, iv), CryptoStreamMode.Read))
            {
                // Read the encrypted values into the decrypted stream. Decrypts the content.
                decryptor.Read(decrypted, 0, decrypted.Length);
            }

            // Create the zip stream to decompress.
            using(GZipStream zip = new GZipStream(new MemoryStream(decrypted), CompressionMode.Decompress, false))
            {
                // Read all bytes in the zip stream and return them.
                return ReadAllBytes(zip);
            }
        }

        /// <summary>
        /// Reads all bytes in the given zip stream and returns them.
        /// </summary>
        /// <param name="zip">The zip stream that is processed.</param>
        /// <returns></returns>
        private static byte[] ReadAllBytes(GZipStream zip)
        {
            if (zip == null)
                throw new ArgumentNullException("zip");

            int buffersize = 100;
            byte[] buffer = new byte[buffersize];
            int offset = 0, read = 0, size = 0;
            do
            {
                // If the buffer doesn’t offer enough space left create a new array
                // with the double size. Copy the current buffer content to that array
                // and use that as new buffer.
                if (buffer.Length < size + buffersize)
                {
                    byte[] tmp = new byte[buffer.Length * 2];
                    Array.Copy(buffer, tmp, buffer.Length);
                    buffer = tmp;
                }

                // Read the net chunk of data.
                read = zip.Read(buffer, offset, buffersize);

                // Increment offset and read size.
                offset += buffersize;
                size += read;
            } while (read == buffersize); // Terminate if we read less then the buffer size.

            // Copy only that amount of data to the result that has actually been read!
            byte[] result = new byte[size];
            Array.Copy(buffer, result, size);
            return result;
        }
    }
}
